/*
 * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
 * The software in this package is published under the terms of the CPAL v1.0
 * license, a copy of which has been included with this distribution in the
 * LICENSE.txt file.
 */
package org.mule.runtime.core.util;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * <code>TemplateParser</code> is a simple string parser that will substitute tokens in a string with values supplied in a Map.
 */
public final class TemplateParser {

  public static final String ANT_TEMPLATE_STYLE = "ant";
  public static final String SQUARE_TEMPLATE_STYLE = "square";
  public static final String CURLY_TEMPLATE_STYLE = "curly";
  public static final String WIGGLY_MULE_TEMPLATE_STYLE = "mule";

  private static final String DOLLAR_ESCAPE = "@@@";
  private static final String NULL_AS_STRING = "null";

  private static final Map<String, PatternInfo> patterns = new HashMap<>();

  static {
    patterns.put(ANT_TEMPLATE_STYLE, new PatternInfo(ANT_TEMPLATE_STYLE, "\\$\\{[^\\{\\}]+\\}", "${", "}"));
    patterns.put(SQUARE_TEMPLATE_STYLE, new PatternInfo(SQUARE_TEMPLATE_STYLE, "\\[[^\\[\\]]+\\]", "[", "]"));
    patterns.put(CURLY_TEMPLATE_STYLE, new PatternInfo(CURLY_TEMPLATE_STYLE, "\\{[^\\{\\}}]+\\}", "{", "}"));

    // Such a complex regex is needed to support nested expressions, otherwise we
    // have to do this manually or using an ANTLR grammar etc.

    // Support for 6 levels (5 nested)
    patterns.put(WIGGLY_MULE_TEMPLATE_STYLE,
                 new PatternInfo(WIGGLY_MULE_TEMPLATE_STYLE,
                                 "#\\[((?:#?\\[(?:#?\\[(?:#?\\[(?:#?\\[(?:#?\\[.*?\\]|[^\\[\\]])*?\\]|[^\\[\\]])*?\\]|[^\\[\\]])*?\\]|[^\\[\\]])*?\\]|[^\\[\\]])*?)\\]",
                                 "#[", "]"));
  }

  /**
   * logger used by this class
   */
  protected static final Logger logger = LoggerFactory.getLogger(TemplateParser.class);

  private final Pattern pattern;
  private final int pre;
  private final int post;
  private final PatternInfo style;


  public static TemplateParser createAntStyleParser() {
    return new TemplateParser(ANT_TEMPLATE_STYLE);
  }

  public static TemplateParser createSquareBracesStyleParser() {
    return new TemplateParser(SQUARE_TEMPLATE_STYLE);
  }

  public static TemplateParser createMuleStyleParser() {
    return new TemplateParser(WIGGLY_MULE_TEMPLATE_STYLE);
  }

  private TemplateParser(String styleName) {
    this.style = patterns.get(styleName);
    if (this.style == null) {
      throw new IllegalArgumentException("Unknown template style: " + styleName);

    }
    pattern = style.getPattern();
    pre = style.getPrefix().length();
    post = style.getSuffix().length();
  }

  /**
   * Matches one or more templates against a Map of key value pairs. If a value for a template is not found in the map the
   * template is left as is in the return String
   *
   * @param props the key/value pairs to match against
   * @param template the string containing the template place holders i.e. My name is ${name}
   * @return the parsed String
   */
  public String parse(Map<?, ?> props, String template) {
    return parse(props, template, null);
  }

  /**
   * Matches one or more templates against a Map of key value pairs. If a value for a template is not found in the map the
   * template is left as is in the return String
   *
   * @param callback a callback used to resolve the property name
   * @param template the string containing the template place holders i.e. My name is ${name}
   * @return the parsed String
   */
  public String parse(TemplateCallback callback, String template) {
    return parse(null, template, callback);
  }

  protected String parse(Map<?, ?> props, String template, TemplateCallback callback) {
    String result = template;
    Map<?, ?> newProps = props;
    if (props != null && !(props instanceof CaseInsensitiveHashMap)) {
      newProps = new CaseInsensitiveHashMap(props);
    }

    Matcher m = pattern.matcher(result);

    while (m.find()) {
      Object value = null;

      String match = m.group();
      String propname = match.substring(pre, match.length() - post);

      if (callback != null) {
        value = callback.match(propname);
        if (value == null) {
          value = NULL_AS_STRING;
        }
      } else if (newProps != null) {
        value = newProps.get(propname);
      }

      if (value == null) {
        if (logger.isDebugEnabled()) {
          logger.debug("Value " + propname + " not found in context");
        }
      } else {
        String matchRegex = Pattern.quote(match);
        String valueString = value.toString();
        // need to escape $ as they resolve into group references, escaping them was not enough
        // This smells a bit like a hack, but one way or another these characters need to be escaped
        if (valueString.indexOf('$') != -1) {
          valueString = valueString.replaceAll("\\$", DOLLAR_ESCAPE);
        }

        if (valueString.indexOf('\\') != -1) {
          valueString = valueString.replaceAll("\\\\", "\\\\\\\\");
        }

        result = result.replaceAll(matchRegex, valueString);
      }
    }
    if (result.indexOf(DOLLAR_ESCAPE) != -1) {
      result = result.replaceAll(DOLLAR_ESCAPE, "\\$");
    }
    return result;
  }

  /**
   * Matches one or more templates against a Map of key value pairs. If a value for a template is not found in the map the
   * template is left as is in the return String
   *
   * @param props the key/value pairs to match against
   * @param templates A List of templates
   * @return the parsed String
   */
  public List<?> parse(Map<?, ?> props, List<?> templates) {
    if (templates == null) {
      return new ArrayList<>();
    }

    List<String> list = new ArrayList<>(templates.size());
    templates.stream().map(tmpl -> parse(props, tmpl.toString())).forEach(list::add);
    return list;
  }

  /**
   * Matches one or more templates against a Map of key value pairs. If a value for a template is not found in the map the
   * template is left as is in the return String
   *
   * @param props the key/value pairs to match against
   * @param templates A Map of templates. The values for each map entry will be parsed
   * @return the parsed String
   */
  public Map<?, ?> parse(final Map<?, ?> props, Map<?, ?> templates) {
    return parse(token -> props.get(token), templates);
  }

  public Map<?, ?> parse(TemplateCallback callback, Map<?, ?> templates) {
    if (templates == null) {
      return new HashMap<>();
    }

    Map<Object, String> map = new HashMap<>(templates.size());
    for (Map.Entry<?, ?> entry : templates.entrySet()) {
      map.put(entry.getKey(), parse(callback, entry.getValue().toString()));
    }
    return map;
  }

  public PatternInfo getStyle() {
    return style;
  }

  public boolean isContainsTemplate(String value) {
    if (value == null) {
      return false;
    }

    Matcher m = pattern.matcher(value);
    return m.find();
  }

  public boolean isValid(String expression) {
    try {
      style.validate(expression);
      return true;
    } catch (IllegalArgumentException e) {
      return false;
    }
  }

  public void validate(String expression) throws IllegalArgumentException {
    style.validate(expression);
  }

  @FunctionalInterface
  public interface TemplateCallback {

    Object match(String token);
  }


  public static class PatternInfo {

    String name;
    String regEx;
    String prefix;
    String suffix;

    PatternInfo(String name, String regEx, String prefix, String suffix) {
      this.name = name;
      this.regEx = regEx;
      if (prefix.length() < 1 || prefix.length() > 2) {
        throw new IllegalArgumentException("Prefix can only be one or two characters long: " + prefix);
      }
      this.prefix = prefix;
      if (suffix.length() != 1) {
        throw new IllegalArgumentException("Suffix can only be one character long: " + suffix);
      }
      this.suffix = suffix;
    }

    public String getRegEx() {
      return regEx;
    }

    public String getPrefix() {
      return prefix;
    }

    public String getSuffix() {
      return suffix;
    }

    public String getName() {
      return name;
    }

    public Pattern getPattern() {
      return Pattern.compile(regEx, Pattern.CASE_INSENSITIVE);
    }

    public void validate(String expression) throws IllegalArgumentException {
      String currentExpression = expression;
      int lastMatchIdx = 0;
      while (lastMatchIdx < expression.length()) {
        int start = currentExpression.indexOf(prefix);
        if (start == -1) {
          // no more expressions to validate
          break;
        }
        lastMatchIdx += start;
        currentExpression = currentExpression.substring(start);
        Matcher m = getPattern().matcher(currentExpression);
        boolean found = m.find();
        if (found) {
          if (!currentExpression.startsWith(m.group())) {
            throw new IllegalArgumentException("Invalid Expression");
          }
          int matchSize = m.group().length();
          lastMatchIdx += matchSize;
          currentExpression = currentExpression.substring(matchSize);
        } else {
          throw new IllegalArgumentException("Invalid Expression");
        }
      }
    }

  }
}
